-
Notifications
You must be signed in to change notification settings - Fork 155
feat: support the oneOf directive #1308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
It means validation of operations and values being sent with operations.
WalkthroughAdds a built-in GraphQL directive Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client
participant Parser
participant Validator
participant Errors
Client->>Parser: submit operation (literal or variables)
Parser->>Validator: AST + values
alt InputObject not annotated
Validator->>Validator: normal input validation
else InputObject has @oneOf
Validator->>Validator: count provided fields
opt count != 1
Validator->>Errors: create ErrOneOfInputObjectFieldCount
Errors-->>Client: return validation error (short-circuit)
end
opt single field is null
Validator->>Errors: create ErrOneOfInputObjectNullValue
Errors-->>Client: return validation error (short-circuit)
end
opt single field uses nullable variable
Validator->>Errors: create ErrOneOfInputObjectNullableVariable
Errors-->>Client: return validation error (short-circuit)
end
Validator->>Validator: continue deeper validation if no violations
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (8)
v2/pkg/astvalidation/operation_rule_values.go (3)
472-538
: Consider extracting the variable nullable check logic.The nullable variable validation logic (lines 508-535) within
objectValueViolatesOneOf
is quite lengthy and could benefit from being extracted into a separate helper method for better readability and testability.Consider refactoring the nullable variable checking into a helper method:
func (v *valuesVisitor) objectValueViolatesOneOf(objectValue ast.Value, defRef int) bool { def := v.definition.InputObjectTypeDefinitions[defRef] // Check if the input object type has @oneOf directive if !def.HasDirectives { return false } hasOneOfDirective := def.Directives.HasDirectiveByName(v.definition, "oneOf") if !hasOneOfDirective { return false } fieldRefs := v.operation.ObjectValues[objectValue.Ref].Refs if len(fieldRefs) != 1 { objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) v.Report.AddExternalError(operationreport.ErrOneOfInputObjectFieldCount(objName, len(fieldRefs), objectValue.Position)) return true } - type nullableVar struct { - fieldRef int - fieldValue ast.Value - variableDefinitionRef int - } - var nullableVars []nullableVar - - for _, fieldRef := range fieldRefs { - fieldValue := v.operation.ObjectFieldValue(fieldRef) - - if fieldValue.Kind == ast.ValueKindNull { - objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) - fieldName := v.operation.ObjectFieldNameBytes(fieldRef) - v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullValue(objName, fieldName, fieldValue.Position)) - return true - } - - if fieldValue.Kind == ast.ValueKindVariable { - // For variables, check if the variable type is nullable - variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(fieldValue.Ref) - if !ok { - continue - } - - // Collect nullable variables - if v.operation.Types[variableTypeRef].TypeKind != ast.TypeKindNonNull { - nullableVars = append(nullableVars, nullableVar{ - fieldRef, - fieldValue, - variableDefinitionRef}) - } - } - } - - // If exactly one field, but it's a nullable variable, report that error - if len(nullableVars) > 0 { - violation := nullableVars[0] - objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) - fieldName := v.operation.ObjectFieldNameBytes(violation.fieldRef) - variableName := v.operation.VariableValueNameBytes(violation.fieldValue.Ref) - v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullableVariable( - objName, fieldName, variableName, violation.fieldValue.Position, - v.operation.VariableDefinitions[violation.variableDefinitionRef].VariableValue.Position)) - return true - } + // Check single field value + fieldRef := fieldRefs[0] + fieldValue := v.operation.ObjectFieldValue(fieldRef) + + if fieldValue.Kind == ast.ValueKindNull { + objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) + fieldName := v.operation.ObjectFieldNameBytes(fieldRef) + v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullValue(objName, fieldName, fieldValue.Position)) + return true + } + + if v.isNullableVariableViolation(fieldRef, fieldValue, defRef) { + return true + } return false } + +func (v *valuesVisitor) isNullableVariableViolation(fieldRef int, fieldValue ast.Value, defRef int) bool { + if fieldValue.Kind != ast.ValueKindVariable { + return false + } + + // For variables, check if the variable type is nullable + variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(fieldValue.Ref) + if !ok { + return false + } + + // Check if nullable + if v.operation.Types[variableTypeRef].TypeKind == ast.TypeKindNonNull { + return false + } + + // Report nullable variable violation + objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) + fieldName := v.operation.ObjectFieldNameBytes(fieldRef) + variableName := v.operation.VariableValueNameBytes(fieldValue.Ref) + v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullableVariable( + objName, fieldName, variableName, fieldValue.Position, + v.operation.VariableDefinitions[variableDefinitionRef].VariableValue.Position)) + return true +}
433-436
: Hooking @OneOf into input-object validation looks correct.The placement after duplicate-field checks is sensible. Note: in variables validation the @OneOf check runs before the “unknown field” check, while here it runs after. If you want consistent error precedence across both paths, consider moving this block before “unknown field” checks.
472-538
: Early-return on first nullable-variable found to avoid allocations.You don’t need to collect all nullable variables; returning on the first violation is enough and avoids slice allocation.
Apply this diff:
func (v *valuesVisitor) objectValueViolatesOneOf(objectValue ast.Value, defRef int) bool { @@ - type nullableVar struct { - fieldRef int - fieldValue ast.Value - variableDefinitionRef int - } - var nullableVars []nullableVar - for _, fieldRef := range fieldRefs { fieldValue := v.operation.ObjectFieldValue(fieldRef) if fieldValue.Kind == ast.ValueKindNull { objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) fieldName := v.operation.ObjectFieldNameBytes(fieldRef) v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullValue(objName, fieldName, fieldValue.Position)) return true } if fieldValue.Kind == ast.ValueKindVariable { // For variables, check if the variable type is nullable variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(fieldValue.Ref) if !ok { continue } - // Collect nullable variables - if v.operation.Types[variableTypeRef].TypeKind != ast.TypeKindNonNull { - nullableVars = append(nullableVars, nullableVar{ - fieldRef, - fieldValue, - variableDefinitionRef}) - } + if v.operation.Types[variableTypeRef].TypeKind != ast.TypeKindNonNull { + objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) + fieldName := v.operation.ObjectFieldNameBytes(fieldRef) + variableName := v.operation.VariableValueNameBytes(fieldValue.Ref) + v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullableVariable( + objName, fieldName, variableName, fieldValue.Position, + v.operation.VariableDefinitions[variableDefinitionRef].VariableValue.Position)) + return true + } } } - // If exactly one field, but it's a nullable variable, report that error - if len(nullableVars) > 0 { - violation := nullableVars[0] - objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) - fieldName := v.operation.ObjectFieldNameBytes(violation.fieldRef) - variableName := v.operation.VariableValueNameBytes(violation.fieldValue.Ref) - v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullableVariable( - objName, fieldName, variableName, violation.fieldValue.Position, - v.operation.VariableDefinitions[violation.variableDefinitionRef].VariableValue.Position)) - return true - } - return false }v2/pkg/variablesvalidation/variablesvalidation.go (2)
375-421
: Consider improving variable naming and field counting logic.While the implementation is functionally correct, there are a few areas that could be improved:
- The variable name
firstNullField
could be more descriptive (e.g.,nullFieldName
)- The
obj.Visit
iteration could be optimized to early exit once a null field is found- The field counting could be simplified since
obj.Len()
already provides the total countConsider these improvements for better clarity and efficiency:
func (v *variablesVisitor) violatesOneOfConstraint(inputObjectDefRef int, jsonValue *astjson.Value, typeName []byte) bool { def := v.definition.InputObjectTypeDefinitions[inputObjectDefRef] // Check if the input object type has @oneOf directive if !def.HasDirectives { return false } hasOneOfDirective := def.Directives.HasDirectiveByName(v.definition, "oneOf") if !hasOneOfDirective { return false } // Count all fields in the JSON object obj := jsonValue.GetObject() totalFieldCount := obj.Len() // Prioritize the count error if totalFieldCount != 1 { variableContent := string(jsonValue.MarshalTo(nil)) v.err = v.newInvalidVariableError( fmt.Sprintf(`%s; OneOf input object "%s" must have exactly one field provided, but %d fields were provided.`, v.invalidValueMessage(string(v.currentVariableName), variableContent), string(typeName), totalFieldCount)) return true } - // Try to find at least one null field. - var firstNullField []byte + // Check if the single field has a null value + var nullFieldName []byte obj.Visit(func(key []byte, val *astjson.Value) { - if firstNullField == nil && val.Type() == astjson.TypeNull { - firstNullField = key + if val.Type() == astjson.TypeNull { + nullFieldName = key } }) - // Check if we have exactly one field, and it's non-null - if firstNullField == nil { + // If the field is non-null, the constraint is satisfied + if nullFieldName == nil { return false } variableContent := string(jsonValue.MarshalTo(nil)) v.err = v.newInvalidVariableError( fmt.Sprintf(`%s; OneOf input object "%s" field "%s" value must be non-null.`, v.invalidValueMessage(string(v.currentVariableName), variableContent), - string(typeName), string(firstNullField))) + string(typeName), string(nullFieldName))) return true }
375-421
: Include the JSON path in @OneOf error messages for better DX.Other errors include the variable path (at "..."). Adding it here improves consistency and debuggability, especially for lists.
Apply this diff:
func (v *variablesVisitor) violatesOneOfConstraint(inputObjectDefRef int, jsonValue *astjson.Value, typeName []byte) bool { @@ - if totalFieldCount != 1 { - variableContent := string(jsonValue.MarshalTo(nil)) - v.err = v.newInvalidVariableError( - fmt.Sprintf(`%s; OneOf input object "%s" must have exactly one field provided, but %d fields were provided.`, - v.invalidValueMessage(string(v.currentVariableName), variableContent), - string(typeName), totalFieldCount)) + if totalFieldCount != 1 { + variableContent := string(jsonValue.MarshalTo(nil)) + var path string + if len(v.path) > 1 { + path = fmt.Sprintf(` at "%s"`, v.renderPath()) + } + v.err = v.newInvalidVariableError( + fmt.Sprintf(`%s%s; OneOf input object "%s" must have exactly one field provided, but %d fields were provided.`, + v.invalidValueMessage(string(v.currentVariableName), variableContent), + path, string(typeName), totalFieldCount)) return true } @@ - variableContent := string(jsonValue.MarshalTo(nil)) - v.err = v.newInvalidVariableError( - fmt.Sprintf(`%s; OneOf input object "%s" field "%s" value must be non-null.`, - v.invalidValueMessage(string(v.currentVariableName), variableContent), - string(typeName), string(firstNullField))) + variableContent := string(jsonValue.MarshalTo(nil)) + var path string + if len(v.path) > 1 { + path = fmt.Sprintf(` at "%s"`, v.renderPath()) + } + v.err = v.newInvalidVariableError( + fmt.Sprintf(`%s%s; OneOf input object "%s" field "%s" value must be non-null.`, + v.invalidValueMessage(string(v.currentVariableName), variableContent), + path, string(typeName), string(firstNullField))) return true }v2/pkg/astvalidation/operation_validation_test.go (2)
5255-5260
: Clarify the @OneOf directive description.Consider stating the behavioral contract in the docstring to aid readers/tools that surface descriptions via introspection.
Apply this diff:
-""" -The @oneOf built-in directive is used within the type system definition language -to indicate an Input Object is a OneOf Input Object. -""" +""" +The @oneOf built-in directive marks an input object as a OneOf Input Object. +Exactly one field must be provided and its value must be non-null at runtime. +All fields defined within a @oneOf input must be nullable in the schema. +""" directive @oneOf on INPUT_OBJECT
3108-3171
: Good coverage; rename duplicate test and relax brittle assertions.
- Two tests share the name “oneOf with null field”; rename the second for clarity.
- The “oneOf with one null field” and “oneOf with two fields” cases assert the exact count suffix (“but 2 fields were provided”), which may be brittle if validation ordering changes. Matching only the invariant prefix is safer.
Apply this diff:
- t.Run("oneOf with null field", func(t *testing.T) { + t.Run("oneOf with both fields null", func(t *testing.T) { run(t, ` mutation oneOfWithNoFields { addPet(pet: { cat: null, dog: null }) { name } }`, Values(), Invalid, withValidationErrors(`OneOf input object "PetInput" must have exactly one field provided, but 2 fields were provided`)) })- }`, Values(), Invalid, - withValidationErrors(`OneOf input object "PetInput" must have exactly one field provided, but 2 fields were provided`)) + }`, Values(), Invalid, + withValidationErrors(`OneOf input object "PetInput" must have exactly one field provided`)) })- }`, Values(), Invalid, - withValidationErrors(`OneOf input object "PetInput" must have exactly one field provided, but 2 fields were provided`)) + }`, Values(), Invalid, + withValidationErrors(`OneOf input object "PetInput" must have exactly one field provided`)) })Optionally, add a positive list case to complement the negative one:
t.Run("list of oneOf with nullable variable", func(t *testing.T) { run(t, ` mutation listOfOneOfWithNullableVariable($dog: DogInput) { addPets(pets: [{ dog: $dog }]) { name } }`, Values(), Invalid, withValidationErrors(`OneOf input object "PetInput" field "dog" cannot use nullable variable "$dog". Variables used in oneOf fields must be non-nullable`)) }) + t.Run("list of oneOf with non-null variable", func(t *testing.T) { + run(t, ` + mutation listOfOneOfWithNonNullVariable($dog: DogInput!) { + addPets(pets: [{ dog: $dog }]) { + name + } + }`, Values(), Valid) + })v2/pkg/variablesvalidation/variablesvalidation_test.go (1)
1494-1712
: Great coverage for @OneOf; consider a couple more edge cases.
- Looks solid: valid/invalid, nesting, lists, and variable-content-enabled cases are well covered.
- Optional: Add a case where a list element is an empty object {} (field count = 0) to ensure the per-element error is surfaced with list context.
- Optional: Add a case with an unknown field alongside a valid field on a @OneOf object to document precedence vs. other validations (unknown field vs. oneOf count). This can help align with operation value validation precedence.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
execution/engine/engine_config_test.go
(1 hunks)execution/engine/testdata/full_introspection.json
(1 hunks)execution/engine/testdata/full_introspection_with_deprecated.json
(1 hunks)execution/engine/testdata/full_introspection_with_typenames.json
(1 hunks)v2/pkg/asttransform/baseschema.go
(1 hunks)v2/pkg/asttransform/fixtures/complete.golden
(1 hunks)v2/pkg/asttransform/fixtures/custom_query_name.golden
(1 hunks)v2/pkg/asttransform/fixtures/mutation_only.golden
(1 hunks)v2/pkg/asttransform/fixtures/schema_missing.golden
(1 hunks)v2/pkg/asttransform/fixtures/simple.golden
(1 hunks)v2/pkg/asttransform/fixtures/subscription_only.golden
(1 hunks)v2/pkg/asttransform/fixtures/subscription_renamed.golden
(1 hunks)v2/pkg/asttransform/fixtures/with_mutation_subscription.golden
(1 hunks)v2/pkg/astvalidation/operation_rule_values.go
(2 hunks)v2/pkg/astvalidation/operation_validation_test.go
(4 hunks)v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection.golden
(1 hunks)v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection_with_custom_root_operation_types.golden
(1 hunks)v2/pkg/federation/fixtures/federated_schema.golden
(1 hunks)v2/pkg/operationreport/externalerror.go
(2 hunks)v2/pkg/variablesvalidation/variablesvalidation.go
(2 hunks)v2/pkg/variablesvalidation/variablesvalidation_test.go
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
v2/pkg/astvalidation/operation_rule_values.go (2)
v2/pkg/operationreport/externalerror.go (3)
ErrOneOfInputObjectFieldCount
(221-226)ErrOneOfInputObjectNullValue
(228-238)ErrOneOfInputObjectNullableVariable
(239-253)pkg/ast/ast_val_variable_value.go (1)
VariableValue
(13-16)
v2/pkg/astvalidation/operation_validation_test.go (3)
v2/pkg/astvalidation/operation_rule_values.go (1)
Values
(15-24)pkg/astvalidation/operation_rule_values.go (1)
Values
(14-23)pkg/astvalidation/validation_state.go (2)
Valid
(10-10)Invalid
(11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Build and test (go 1.25 / windows-latest)
- GitHub Check: Build and test (go 1.25 / ubuntu-latest)
- GitHub Check: Build and test (go 1.25 / ubuntu-latest)
- GitHub Check: Build and test (go 1.25 / windows-latest)
🔇 Additional comments (27)
v2/pkg/asttransform/fixtures/schema_missing.golden (1)
60-64
: Schema fixture picks up @OneOf correctlyThanks for keeping the fixture in lock‑step with the spec-aligned directive docstring and location.
v2/pkg/asttransform/baseschema.go (1)
171-175
: Base schema now exposes @OneOfNice to see the built-in directive wired into the canonical SDL; this keeps downstream merges consistent.
v2/pkg/asttransform/fixtures/subscription_only.golden (1)
51-55
: Subscription-only fixture matches the base schemaDirective and description look consistent here as well.
v2/pkg/asttransform/fixtures/mutation_only.golden (1)
52-56
: Mutation fixture updated cleanlyYep, mirrors the shared SDL addition.
execution/engine/testdata/full_introspection_with_typenames.json (1)
834-843
: Introspection output now surfaces @OneOfThe directive entry lines up with the schema definition; introspection clients will see it.
v2/pkg/asttransform/fixtures/simple.golden (1)
60-64
: Simple fixture synchronizedEverything stays consistent with the base schema.
v2/pkg/asttransform/fixtures/subscription_renamed.golden (1)
51-55
: Renamed subscription fixture includes @OneOfLooks good—helps keep every golden in sync.
v2/pkg/federation/fixtures/federated_schema.golden (1)
100-104
: Federated schema exposes @OneOfAppreciate the consistency across federation fixtures, too.
execution/engine/testdata/full_introspection.json (1)
738-746
: Introspection wiring for@oneOf
looks consistentThe new directive entry mirrors the base SDL definition—no args, scoped to
INPUT_OBJECT
, and the description matches the spec wording. Fixtures stay aligned.v2/pkg/asttransform/fixtures/complete.golden (1)
60-64
: Base SDL now exposes@oneOf
Adding the documented directive right after
@specifiedBy
keeps the generated schema in sync with the GraphQL update.execution/engine/engine_config_test.go (1)
402-406
: Engine test schema updated appropriatelyThe test schema now contains the
@oneOf
directive with the canonical description and location, matching the other fixtures.v2/pkg/asttransform/fixtures/with_mutation_subscription.golden (1)
71-75
: Fixture correctly includes@oneOf
Directive definition and placement align with the broader schema fixtures, so introspection and transforms will remain coherent.
v2/pkg/asttransform/fixtures/custom_query_name.golden (1)
60-64
: Custom-query fixture stays in syncNice to see the one-of directive propagated here too; no mismatches with the base schema.
execution/engine/testdata/full_introspection_with_deprecated.json (1)
772-779
: Deprecated-introspection fixture updatedDirective entry mirrors the others (description, locations, empty args), so the introspection snapshot remains accurate.
v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection_with_custom_root_operation_types.golden (1)
500-510
: Custom-root introspection fixture aligned
oneOf
directive appears with the expected metadata (isRepeatable: false
,INPUT_OBJECT
location). Looks good.v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection.golden (1)
352-362
: Primary introspection fixture reflects new directiveEntry structure matches the rest—no args, not repeatable, scoped to
INPUT_OBJECT
. Everything lines up.v2/pkg/variablesvalidation/variablesvalidation_test.go (1)
1495-1713
: LGTM! Comprehensive test coverage for the oneOf directive.The test suite thoroughly covers the oneOf constraint validation including:
- Valid single-field cases with different field types
- Invalid cases (no fields, multiple fields, null values)
- Nested oneOf structures
- Lists of oneOf input objects
- Edge cases like empty lists and nullable variables
The error messages are clear and descriptive, properly indicating the specific constraint violations.
v2/pkg/astvalidation/operation_rule_values.go (2)
433-438
: LGTM! Well-integrated oneOf validation in input object processing.The validation is properly placed after duplicate field checks and before returning success, ensuring the oneOf constraint is enforced at the appropriate stage of value validation.
472-538
: Confirm spec intent for defaulted nullable variables.Current logic flags a violation when the single provided field is a nullable variable type, even if the variable has a non-null default. Please confirm this matches the intended @OneOf spec semantics in this repo (i.e., “variable must be Non-Null” vs. “effective non-null via default is acceptable”), and consider adding a test covering a nullable variable with a non-null default.
v2/pkg/variablesvalidation/variablesvalidation.go (2)
449-452
: LGTM! Clean integration point for oneOf validation.The oneOf constraint check is properly integrated into the input object traversal, with early return on violation to prevent further processing.
449-451
: Integration point is correct.Running the @OneOf check after field-type traversal ensures type errors are caught first, then structural @OneOf violations are reported.
v2/pkg/astvalidation/operation_validation_test.go (2)
5044-5063
: SDL additions for @OneOf look correct.
- PetInput carries nullable fields (cat, dog) as required by oneOf.
- New mutations return the interface Pet, which is valid in SDL-only validation tests.
4079-4107
: Approve oneOf nullability tests; non-null vars may have defaults
DefaultValue on non-null variable types is legal per GraphQL spec’s VariableDefinition grammar and coercion rules.v2/pkg/operationreport/externalerror.go (4)
28-30
: New oneOf error messages read cleanlyThanks for providing clear, specific templates for each oneOf validation failure; the language matches the rest of the file and keeps the constraint unambiguous.
221-226
: Field-count validation helper looks solidThe helper mirrors existing patterns (message constant + location capture) and keeps the off-by-one messaging precise. No issues spotted.
228-238
: Null-value constructor aligns with expectationsCapturing the field location explicitly gives clients enough context when a null literal sneaks in. Implementation is consistent with nearby helpers.
239-253
: Nullable-variable helper provides great diagnosticsIncluding both the field and variable definition locations will make debugging much easier, and the message text is spot on. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
v2/pkg/variablesvalidation/variablesvalidation.go (1)
375-433
: Variables-side @OneOf enforcement looks correct; minor polish suggested.
- Extract directive name "oneOf" to a shared constant for reuse and to avoid typos.
- Tiny nit: rename “Count all fields in the JSON object” comment to “Count provided fields” for clarity.
v2/pkg/variablesvalidation/variablesvalidation_test.go (1)
1495-1711
: Add operation-validation test for @OneOf with nullable variable
No existing coverage forpet(input:{cat: $cat})
where$cat: String
; add a test in v2/pkg/astvalidation/operation_validation_test.go exercising ErrOneOfInputObjectNullableVariable.v2/pkg/astvalidation/operation_validation_test.go (1)
5068-5071
: LGTM: PetInput annotated with @OneOfFields are nullable as required by the oneOf contract.
Consider adding a negative test (elsewhere) that asserts schema-level rejection if a @OneOf input defines any Non‑Null field, to guard the schema rule too.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
execution/engine/engine_config_test.go
(1 hunks)v2/pkg/asttransform/baseschema.go
(1 hunks)v2/pkg/asttransform/fixtures/complete.golden
(1 hunks)v2/pkg/asttransform/fixtures/custom_query_name.golden
(1 hunks)v2/pkg/asttransform/fixtures/mutation_only.golden
(1 hunks)v2/pkg/asttransform/fixtures/schema_missing.golden
(1 hunks)v2/pkg/asttransform/fixtures/simple.golden
(1 hunks)v2/pkg/asttransform/fixtures/subscription_only.golden
(1 hunks)v2/pkg/asttransform/fixtures/subscription_renamed.golden
(1 hunks)v2/pkg/asttransform/fixtures/with_mutation_subscription.golden
(1 hunks)v2/pkg/astvalidation/operation_rule_values.go
(2 hunks)v2/pkg/astvalidation/operation_validation_test.go
(4 hunks)v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection.golden
(1 hunks)v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection_with_custom_root_operation_types.golden
(1 hunks)v2/pkg/federation/fixtures/federated_schema.golden
(1 hunks)v2/pkg/variablesvalidation/variablesvalidation.go
(2 hunks)v2/pkg/variablesvalidation/variablesvalidation_test.go
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
- v2/pkg/asttransform/fixtures/with_mutation_subscription.golden
- v2/pkg/asttransform/fixtures/subscription_renamed.golden
- v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection.golden
- v2/pkg/asttransform/fixtures/custom_query_name.golden
- v2/pkg/asttransform/fixtures/complete.golden
- v2/pkg/asttransform/fixtures/schema_missing.golden
- v2/pkg/asttransform/baseschema.go
- execution/engine/engine_config_test.go
- v2/pkg/asttransform/fixtures/simple.golden
🧰 Additional context used
🧬 Code graph analysis (2)
v2/pkg/astvalidation/operation_rule_values.go (2)
v2/pkg/operationreport/externalerror.go (3)
ErrOneOfInputObjectFieldCount
(221-226)ErrOneOfInputObjectNullValue
(228-238)ErrOneOfInputObjectNullableVariable
(239-253)pkg/ast/ast_val_variable_value.go (1)
VariableValue
(13-16)
v2/pkg/astvalidation/operation_validation_test.go (3)
v2/pkg/astvalidation/operation_rule_values.go (1)
Values
(15-24)pkg/astvalidation/operation_rule_values.go (1)
Values
(14-23)pkg/astvalidation/validation_state.go (2)
Valid
(10-10)Invalid
(11-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build and test (go 1.25 / windows-latest)
- GitHub Check: Build and test (go 1.25 / ubuntu-latest)
🔇 Additional comments (14)
v2/pkg/astvalidation/operation_rule_values.go (2)
433-436
: Good integration point for @OneOf enforcement.Running the @OneOf check after field existence/duplicates and short‑circuiting on violation is correct.
472-523
: Extract constants, clarify comment, add nullable-variable test
- Extract “oneOf” to a shared constant to avoid typos.
- Rename the “Collect nullable variables” comment to reflect that it reports an error on nullable variables.
- (Optional) Add a schema rule enforcing that all fields in a @OneOf input are nullable.
- No tests cover ErrOneOfInputObjectNullableVariable; add a case for “object literal with a variable value of nullable type.”
v2/pkg/engine/datasource/introspection_datasource/fixtures/schema_introspection_with_custom_root_operation_types.golden (1)
501-511
: Introspection updated correctly to include @OneOf.Directive shape (name, description, locations, empty args, isRepeatable) looks consistent with other built‑ins.
v2/pkg/asttransform/fixtures/subscription_only.golden (1)
51-57
: SDL fixture correctly declares @OneOf.Docstring and location align with spec expectations.
v2/pkg/asttransform/fixtures/mutation_only.golden (1)
52-58
: SDL fixture correctly declares @OneOf.Placement and description are consistent with other fixtures.
v2/pkg/federation/fixtures/federated_schema.golden (1)
100-106
: Federation schema fixture updated appropriately.@OneOf is added alongside existing built‑ins with a clear docstring.
v2/pkg/variablesvalidation/variablesvalidation.go (1)
477-479
: Correct placement and short-circuit on @OneOf violation.Runs after field definition checks and exits early on error.
v2/pkg/astvalidation/operation_validation_test.go (7)
3170-3178
: LGTM: correct rejection for nullable variable in oneOf listAccurately enforces “variables used in oneOf fields must be non‑nullable”.
4106-4114
: LGTM: nullable variable correctly rejected for oneOf fieldMatches the new oneOf validation contract and message format.
5052-5054
: LGTM: mutation entry points for oneOf tests
addPet
andaddPets
signatures are coherent with the introduced inputs and existingPet
interface.
5056-5066
: LGTM: supporting CatInput/DogInput for oneOfShapes align with usage; required
name
fields enable meaningful validation.
5263-5269
: LGTM: @OneOf directive definition and docsAccurate location (INPUT_OBJECT) and helpful description.
4098-4105
: Remove suggested change: default values on Non-Null GraphQL variables are allowed
Current GraphQL spec (June 2018+) permits default values on Non-Null variable definitions, so$cat: CatInput! = { name: "black" }
is valid and requires no change.Likely an incorrect or invalid review comment.
3109-3116
: No change needed: defaults on Non-Null variables are allowed The GraphQL spec permits a Non-Null variable to include a default value (e.g.query Q($n: Int! = 3)
). The existing test cases using$pet: PetInput! = {...}
are spec-valid.Likely an incorrect or invalid review comment.
} | ||
err := runTest(t, tc) | ||
require.Error(t, err) | ||
assert.Contains(t, err.Error(), `Variable "$input" got invalid value; OneOf input object "PetInput" must have exactly one field provided, but 2 fields were provided`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like there should be a colon instead of a semicolon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I followed the example set by errors from other rules. They all use semicolon for multi sentence errors.
} | ||
err := runTest(t, tc) | ||
require.Error(t, err) | ||
assert.Contains(t, err.Error(), `OneOf input object "PetInput" must have exactly one field provided, but 2 fields were provided`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you assert full error message? do we have list item index in the error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like we do not add locations here at all, nore the item index. Figuring out possible fixes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean field.0.otherField
|
||
// objectValueViolatesOneOf checks if an input object value violates the @oneOf directive constraint. | ||
func (v *valuesVisitor) objectValueViolatesOneOf(objectValue ast.Value, defRef int) bool { | ||
def := v.definition.InputObjectTypeDefinitions[defRef] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use longer names like inputObjectTypeDef
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Old habbits die hard. Them I will be fighting for a long time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
v2/pkg/astvalidation/operation_rule_values.go (1)
484-522
: Consider simplifying the field iteration.Since you've already verified
len(fieldRefs) == 1
at line 485, the loop on lines 491-519 will only execute once. You could simplify by accessingfieldRefs[0]
directly:fieldRefs := v.operation.ObjectValues[objectValue.Ref].Refs if len(fieldRefs) != 1 { objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) v.Report.AddExternalError(operationreport.ErrOneOfInputObjectFieldCount(objName, len(fieldRefs), objectValue.Position)) return true } -for _, fieldRef := range fieldRefs { +// Exactly one field is present (verified above) +fieldRef := fieldRefs[0] +{ fieldValue := v.operation.ObjectFieldValue(fieldRef) if fieldValue.Kind == ast.ValueKindNull { objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) fieldName := v.operation.ObjectFieldNameBytes(fieldRef) v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullValue(objName, fieldName, fieldValue.Position)) return true } if fieldValue.Kind == ast.ValueKindVariable { // For variables, check if the variable type is nullable variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(fieldValue.Ref) if !ok { continue } // Collect nullable variables if v.operation.Types[variableTypeRef].TypeKind != ast.TypeKindNonNull { objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) fieldName := v.operation.ObjectFieldNameBytes(fieldRef) variableName := v.operation.VariableValueNameBytes(fieldValue.Ref) v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullableVariable( objName, fieldName, variableName, fieldValue.Position, v.operation.VariableDefinitions[variableDefinitionRef].VariableValue.Position)) return true } } } return falseHowever, the current loop-based approach is defensive and consistent with patterns used elsewhere in this file, so this is purely optional.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
v2/pkg/astvalidation/operation_rule_values.go
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
v2/pkg/astvalidation/operation_rule_values.go (2)
v2/pkg/operationreport/externalerror.go (3)
ErrOneOfInputObjectFieldCount
(221-226)ErrOneOfInputObjectNullValue
(228-238)ErrOneOfInputObjectNullableVariable
(239-253)pkg/ast/ast_val_variable_value.go (1)
VariableValue
(13-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Build and test (go 1.25 / windows-latest)
- GitHub Check: Build and test (go 1.25 / ubuntu-latest)
- GitHub Check: Build and test (go 1.25 / ubuntu-latest)
- GitHub Check: Build and test (go 1.25 / windows-latest)
🔇 Additional comments (2)
v2/pkg/astvalidation/operation_rule_values.go (2)
433-436
: LGTM! Integration point is well-placed.The oneOf validation is correctly positioned after all field-level validations and duplicate checks, ensuring structural constraints are enforced last. The short-circuit on violation is appropriate.
472-482
: LGTM! Directive detection is correct.The function properly checks for the
@oneOf
directive and returns early if not present, avoiding unnecessary validation overhead for regular input objects. Good adherence to the past review feedback on variable naming.
if fieldValue.Kind == ast.ValueKindVariable { | ||
// For variables, check if the variable type is nullable | ||
variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(fieldValue.Ref) | ||
if !ok { | ||
continue | ||
} | ||
|
||
// Collect nullable variables | ||
if v.operation.Types[variableTypeRef].TypeKind != ast.TypeKindNonNull { | ||
objName := v.definition.InputObjectTypeDefinitionNameBytes(defRef) | ||
fieldName := v.operation.ObjectFieldNameBytes(fieldRef) | ||
variableName := v.operation.VariableValueNameBytes(fieldValue.Ref) | ||
v.Report.AddExternalError(operationreport.ErrOneOfInputObjectNullableVariable( | ||
objName, fieldName, variableName, fieldValue.Position, | ||
v.operation.VariableDefinitions[variableDefinitionRef].VariableValue.Position)) | ||
return true | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify behavior when operationVariableType
returns !ok
.
If operationVariableType
fails on line 503, the continue
on line 505 exits the loop, and the function returns false
(no violation) on line 521. This could potentially miss a constraint violation if the variable info cannot be retrieved.
While variables should have been validated earlier in the pipeline, making this edge case unlikely, consider either:
- Adding a comment explaining why
continue
is safe here (e.g., "Variable existence already validated; failure here indicates internal inconsistency") - Or handling the failure more explicitly
The nullable variable detection logic itself (lines 509-517) is correct and properly reports errors with both field and variable positions.
Run the following to verify that undefined variables are caught before oneOf validation:
🏁 Script executed:
#!/bin/bash
# Description: Check if undefined variables are validated before oneOf constraint checking
# Search for variable validation in the validation pipeline
ast-grep --pattern $'func $_(ref int) {
$$$
if !exists {
$$$StopWithExternalErr$$$ErrVariableNotDefinedOnOperation$$$
}
$$$
}'
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for where variables are validated in the validation package
rg -n "ErrVariableNotDefined|ValidateVariable|variable.*validation" v2/pkg/astvalidation/ -A 2 -B 2
Length of output: 1389
🏁 Script executed:
#!/bin/bash
# Look for the operationVariableType method to understand when it returns !ok
rg -n "func.*operationVariableType" v2/pkg/astvalidation/ -A 15
Length of output: 1039
🏁 Script executed:
#!/bin/bash
# Search for where oneOf validation is called in the pipeline
rg -n "oneOf|OneOf" v2/pkg/astvalidation/operation_rule_values.go -B 5 -A 5
Length of output: 2368
🏁 Script executed:
#!/bin/bash
# Find validation entry points and pipeline order
rg -n "ValidateOperation|VisitOperationDefinition" v2/pkg/astvalidation/ -A 3 -B 1 | head -50
Length of output: 0
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the function that contains the early variable validation (lines 55-59)
rg -n "func.*valuesVisitor" v2/pkg/astvalidation/operation_rule_values.go -A 50 | head -80
Length of output: 2913
🏁 Script executed:
#!/bin/bash
# Look for the validation visitor pattern and pipeline order
rg -n "type valuesVisitor" v2/pkg/astvalidation/ -A 10
Length of output: 800
🏁 Script executed:
#!/bin/bash
# Search for where valuesVisitor is instantiated and used
rg -n "valuesVisitor\{" v2/pkg/astvalidation/ -B 5 -A 5
Length of output: 857
🏁 Script executed:
#!/bin/bash
# Check the structure of the values.go file to understand the pipeline
fd "values.go" v2/pkg/astvalidation/ -x wc -l {}
Length of output: 49
🏁 Script executed:
#!/bin/bash
# Find functions that validate object values - need to understand if they're called in sequence
rg -n "func.*visitObjectValue|func.*objectValue|func.*Visit" v2/pkg/astvalidation/operation_rule_values.go | head -20
Length of output: 2003
🏁 Script executed:
#!/bin/bash
# Confirm the all_variable_uses_defined rule validates nested variables
rg -n "VisitObjectField|VisitVariable|EnterObjectField" v2/pkg/astvalidation/operation_rule_all_variable_uses_defined.go -A 3 -B 1
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Find where validation rules are registered/ordered
rg -n "func Validate|AllVariableUsesDefined|Values()" v2/pkg/astvalidation/ --type=go -l | head -10
Length of output: 535
🏁 Script executed:
#!/bin/bash
# Look at the main validation orchestration
fd "*.go" v2/pkg/astvalidation/ -x grep -l "AllVariableUsesDefined\|Values" | grep -v test | grep -v operation_rule
Length of output: 283
🏁 Script executed:
#!/bin/bash
# Search for the rule composition/registration
rg -n "AllVariableUsesDefined|Values\(\)" v2/pkg/astvalidation/operation_validator.go -A 2 -B 2
Length of output: 83
---
🏁 Script executed:
```shell
#!/bin/bash
# Find and examine operation_validation.go
find v2/pkg/astvalidation -name "operation_validation.go" -not -path "*/test*" -type f
Length of output: 44
🏁 Script executed:
#!/bin/bash
# Look for NewOperationValidator or similar to understand rule composition
rg -n "func New|return.*Rule|append.*Rule" v2/pkg/astvalidation/ --type=go | grep -v test | head -30
Length of output: 225
🏁 Script executed:
#!/bin/bash
# Check what the all_variable_uses_defined rule actually does
cat v2/pkg/astvalidation/operation_rule_all_variable_uses_defined.go | head -100
Length of output: 1926
🏁 Script executed:
#!/bin/bash
# Search for where Values rule and variable validation interact
rg -n "EnterObjectField|ObjectField" v2/pkg/astvalidation/operation_rule_values.go -B 2 -A 5
Length of output: 3095
🏁 Script executed:
#!/bin/bash
# Look at the valuesVisitor registration - does it handle object fields?
rg -n "Register.*Visitor|Walker.Register" v2/pkg/astvalidation/operation_rule_values.go
Length of output: 162
🏁 Script executed:
#!/bin/bash
# Search for any rule that validates variables in object field values
rg -n "ObjectField|ObjectValue" v2/pkg/astvalidation/operation_rule*.go -l | xargs grep -l "Variable\|variable"
Length of output: 103
🏁 Script executed:
#!/bin/bash
# Check if operationVariableDefinition can return !exists for nested variables
rg -n "func.*operationVariableDefinition" v2/pkg/astvalidation/ -A 10
Length of output: 1072
🏁 Script executed:
#!/bin/bash
# Look at line 503-505 context more carefully - check surrounding code
sed -n '500,525p' v2/pkg/astvalidation/operation_rule_values.go
Length of output: 964
🏁 Script executed:
#!/bin/bash
# Search for any tests that cover undefined variables in oneOf object fields
rg -n "oneOf.*undefined|oneOf.*variable.*undefined" v2/pkg/astvalidation/operation_validation_test.go -B 3 -A 10
Length of output: 0
Add validation for undefined variables in oneOf object fields or report an error when operationVariableType
fails.
Lines 501-505 silently skip undefined variables without reporting an error. While variables at the argument level are validated by the AllVariableUsesDefined
rule, variables nested in object field values—including those in oneOf fields—are not pre-validated. When operationVariableType
returns !ok
on line 503 (indicating an undefined variable), the continue
on line 505 exits the loop without reporting a violation, and the function returns false
(no constraint violation detected).
This allows undefined variables in oneOf fields to pass validation uncaught. Either:
- Add an error report before the
continue
: report undefined variables as invalid in oneOf fields - Or validate all variables in object values before oneOf constraint checking
🤖 Prompt for AI Agents
In v2/pkg/astvalidation/operation_rule_values.go around lines 501-519, the loop
silently skips when operationVariableType returns !ok for a variable in a oneOf
object field; instead, report the undefined variable as an error before
continuing. Capture the object name, field name, variable name and the variable
definition/usage positions (similar to surrounding error reports) and call
v.Report.AddExternalError to emit an undefined-variable error for this context,
then return true to mark the constraint violation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be the good test to verify your hypothesis?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ysmolski Here's a test case that would verify the hypothesis:
func TestOneOfWithUndefinedVariable(t *testing.T) {
t.Run("oneOf field with undefined variable should fail validation", func(t *testing.T) {
runWithVariables(t, testDefinition, `
mutation AddPet($name: String!) {
addPet(pet: { cat: { name: $name, meowVolume: $undefinedVolume } })
}
`, "", `Variable "$undefinedVolume" is not defined`, func(t *testing.T, err error) {
// Should report that $undefinedVolume is undefined
})
})
}
What this test does:
- Defines a mutation that uses
PetInput
(a oneOf input object) - The
cat
field uses a variable$undefinedVolume
that is not declared in the operation's variable definitions - Only
$name
is defined, but$undefinedVolume
is used
Expected behavior:
- ✅ If validation is working correctly: The test should fail during validation with an error about
$undefinedVolume
being undefined - ❌ If the hypothesis is true: The undefined variable will pass validation silently, and the test will expose the bug
You can add this test to v2/pkg/astvalidation/operation_validation_test.go
alongside the other oneOf tests. If the test passes (meaning validation catches the error), then the current implementation is safe. If it doesn't report the undefined variable error, then the hypothesis is confirmed and needs fixing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about rule AllVariableUsesDefined
? Does it provide what we need?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Get the full implementation of AllVariableUsesDefined rule
cat v2/pkg/astvalidation/operation_rule_all_variable_uses_defined.go
Length of output: 1926
🏁 Script executed:
#!/bin/bash
# Search for how the validation walker handles object fields and values
rg -n "RegisterEnter.*Visitor" v2/pkg/astvalidation/operation_rule_all_variable_uses_defined.go -A 1
Length of output: 107
🏁 Script executed:
#!/bin/bash
# Check if there are any tests that verify AllVariableUsesDefined catches undefined variables in object fields
rg -n "AllVariableUsesDefined|undefined.*variable.*object" v2/pkg/astvalidation/operation_validation_test.go -B 3 -A 10
Length of output: 3040
@ysmolski Good question! After examining the code, here's what I found:
AllVariableUsesDefined
has limited scope:
The rule only registers RegisterEnterArgumentVisitor
(line 18), which means it validates only variables that appear directly as argument values. It does not walk through object fields or nested object values.
However, the Values()
rule provides what we need:
The Values()
rule has its own comprehensive variable validation that catches undefined variables at any nesting level:
-
In
valueSatisfiesInputObjectTypeDefinition
(lines 398-445), the validation flow is:- First: Validate all fields against their definitions (lines 411-415)
- Then: Check for unknown fields (lines 421-431)
- Then: Check for duplicate fields (lines 437-439)
- Finally: Check oneOf constraint (lines 442-445)
-
Field validation catches undefined variables through
variableValueSatisfiesDefinitionType
(lines 164-178):func (v *valuesVisitor) variableValueSatisfiesDefinitionType(value ast.Value, definitionTypeRef int) bool { variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(value.Ref) if !ok { v.handleTypeError(value, definitionTypeRef) // ← Reports error for undefined variables return false } ... }
Conclusion:
By the time the oneOf validation runs on line 442, all variables in the object fields have already been validated by the earlier field validation loop. If operationVariableType
returns !ok
on line 503, it would indicate an internal inconsistency (since the variable should have been caught earlier), making the continue
safe.
Your test suggestion would still be valuable to confirm this behavior and document it as a test case, but the validation should already be working correctly. The test would likely show that the undefined variable error is reported during the field validation phase, before oneOf validation even runs.
🧬 Code Graph Analysis Results
- File: v2/pkg/astvalidation/operation_rule_values.go
- EnterArgument method
func (v *valuesVisitor) EnterArgument(ref int) { definitionRef, exists := v.ArgumentInputValueDefinition(ref) if !exists { return } value := v.operation.ArgumentValue(ref) if value.Kind == ast.ValueKindVariable { _, exists := v.variableDefinition(value.Ref) if !exists { v.StopWithExternalErr(operationreport.ErrVariableNotDefinedOnOperation(v.operation.VariableValueNameBytes(value.Ref), []byte(""))) return } v.validateIfValueSatisfiesInputFieldDefinition(value, definitionRef) return } v.valueSatisfiesInputValueDefinitionType(value, v.definition.InputValueDefinitions[definitionRef].Type) }
- EnterVariableDefinition method
func (v *valuesVisitor) EnterVariableDefinition(ref int) { if !v.operation.VariableDefinitionHasDefaultValue(ref) { return // variable has no default value, deep type check not required } v.valueSatisfiesOperationType(v.operation.VariableDefinitions[ref].DefaultValue.Value, v.operation.VariableDefinitions[ref].Type) }
- operationVariableDefinition helper
func (v *valuesVisitor) operationVariableDefinition(variableValueRef int) (ref int, exists bool) { variableName := v.operation.VariableValueNameBytes(variableValueRef) if v.Ancestors[0].Kind == ast.NodeKindOperationDefinition { return v.operation.VariableDefinitionByNameAndOperation(v.Ancestors[0].Ref, variableName) } for opDefRef := 0; opDefRef < len(v.operation.OperationDefinitions); opDefRef++ { ref, exists = v.operation.VariableDefinitionByNameAndOperation(opDefRef, variableName) if exists { return } } return ast.InvalidRef, false }
- operationVariableType helper
func (v *valuesVisitor) operationVariableType(variableValueRef int) (variableDefinitionRef int, variableTypeRef int, typeName ast.ByteSlice, ok bool) { variableDefRef, exists := v.operationVariableDefinition(variableValueRef) if !exists { return ast.InvalidRef, ast.InvalidRef, nil, false } variableTypeRef = v.operation.VariableDefinitions[variableDefRef].Type typeName = v.operation.ResolveTypeNameBytes(variableTypeRef) return variableDefRef, variableTypeRef, typeName, true }
- EnterArgument method
- File: v2/pkg/operationreport/externalerror.go
- ErrVariableNotDefinedOnOperation
func ErrVariableNotDefinedOnOperation(variableName, operationName ast.ByteSlice) (err ExternalError) { err.Message = fmt.Sprintf("variable: %s not defined on operation: %s", variableName, operationName) return err }
- ErrVariableNotDefinedOnOperation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this check if variable passed to input object field is defined is not done at all, not just for oneOf inputObjects. Can you verify it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for tests that use undefined variables in object/input fields
rg -n "undefined.*variable|variable.*not.*defined" v2/pkg/astvalidation/operation_validation_test.go -B 3 -A 10
Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check what error handleVariableHasIncompatibleTypeError actually reports
rg -n "func.*handleVariableHasIncompatibleTypeError" v2/pkg/astvalidation/operation_rule_values.go -A 20
Length of output: 608
🏁 Script executed:
#!/bin/bash
# Search for any test that has a variable in an object field value
rg -n '\{\s*\w+:\s*\$' v2/pkg/astvalidation/operation_validation_test.go -B 2 -A 5
Length of output: 12431
🏁 Script executed:
#!/bin/bash
# Look for the variableValueHasMatchingTypeName implementation to see what happens when operationVariableType fails
sed -n '553,570p' v2/pkg/astvalidation/operation_rule_values.go
Length of output: 677
🏁 Script executed:
#!/bin/bash
# Create a simple test case to verify undefined variables in object fields are not caught
cat > /tmp/test_undefined_var.go << 'EOF'
package astvalidation
import "testing"
func TestUndefinedVariableInObjectField(t *testing.T) {
t.Run("undefined variable in regular input object field", func(t *testing.T) {
run(t, `
query findDog($name: String!) {
findDog(complex: { name: $name, nickname: $undefinedVar })
}
`, Values(), Valid) // Should be Invalid but likely passes
})
}
EOF
cat /tmp/test_undefined_var.go
Length of output: 357
🏁 Script executed:
#!/bin/bash
# Check how variableValueSatisfiesDefinitionType handles undefined variables
sed -n '164,178p' v2/pkg/astvalidation/operation_rule_values.go
Length of output: 546
🏁 Script executed:
#!/bin/bash
# Look at the full handleVariableHasIncompatibleTypeError to see if it truly doesn't report undefined variables
sed -n '619,650p' v2/pkg/astvalidation/operation_rule_values.go
Length of output: 1056
🏁 Script executed:
#!/bin/bash
# Check if there's any other place where undefined variables in object fields might be caught
rg -n "ErrVariableNotDefined" v2/pkg/astvalidation/ --type=go
Length of output: 350
🏁 Script executed:
#!/bin/bash
# Check what handleTypeError does
sed -n '572,585p' v2/pkg/astvalidation/operation_rule_values.go
Length of output: 504
🏁 Script executed:
#!/bin/bash
# Also check if there are any tests that would have caught this bug
rg -n "complex.*\{.*\$" v2/pkg/astvalidation/operation_validation_test.go | head -20
Length of output: 1240
🏁 Script executed:
#!/bin/bash
# See if printValueAndUnderlyingType works with undefined variables
sed -n '688,705p' v2/pkg/astvalidation/operation_rule_values.go
Length of output: 557
@ysmolski You're absolutely correct! 🎯 This is a broader bug that affects all input object fields with nullable types, not just oneOf inputs.
The Bug
There are two validation paths for variables in the code:
Path 1: NonNull types ✅ (works correctly)
valuesSatisfiesNonNullType (line 234)
→ variableValueSatisfiesDefinitionType (line 168)
→ operationVariableType fails (!ok)
→ handleTypeError (line 170)
→ reports ErrValueDoesntSatisfyType ✅
Path 2: Scalar/Enum/InputObject types ❌ (silently fails)
valueSatisfiesScalar/Enum/InputObject (lines 344/354/403)
→ variableValueHasMatchingTypeName (line 556)
→ operationVariableType fails (!ok)
→ handleVariableHasIncompatibleTypeError (line 558)
→ calls operationVariableType AGAIN (line 630)
→ still fails (!ok on line 631)
→ silently returns (line 632) ❌ NO ERROR REPORTED
The Root Cause
In handleVariableHasIncompatibleTypeError
(lines 619-648):
variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(value.Ref)
if !ok {
return // ← Silent failure!
}
When the variable is undefined, operationVariableType
returns !ok
, and the function exits without reporting any error.
Test Case to Verify
func TestUndefinedVariableInObjectField(t *testing.T) {
t.Run("undefined variable in nullable field - CURRENTLY PASSES, SHOULD FAIL", func(t *testing.T) {
run(t, `
query findDog($name: String!) {
findDog(complex: { name: $name, nickname: $undefinedVar })
}
`, Values(), Invalid, // Should fail but likely passes silently
withValidationErrors(`Variable "$undefinedVar" is not defined`))
})
t.Run("undefined variable in non-null field - ALREADY CAUGHT", func(t *testing.T) {
run(t, `
query findDog($name: String!) {
findDogNonOptional(complex: { name: $undefinedVar })
}
`, Values(), Invalid, // This one is already caught
withValidationErrors(`Value "$undefinedVar" doesn't satisfy type "String"`))
})
}
The Fix
The fix should be in handleVariableHasIncompatibleTypeError
(around line 630-632):
variableDefinitionRef, variableTypeRef, _, ok := v.operationVariableType(value.Ref)
if !ok {
// Report undefined variable error instead of silently returning
variableName := v.operation.VariableValueNameBytes(value.Ref)
v.Report.AddExternalError(operationreport.ErrVariableNotDefinedOnOperation(variableName, []byte("")))
return
}
This would make undefined variables in all contexts report a proper error, not just in oneOf fields or NonNull fields.
Would you like me to create an issue for this broader fix?
🧬 Code Graph Analysis Results
[relevant_snippets]
Filename: v2/pkg/astvalidation/operation_rule_values.go
- Function: objectValueViolatesOneOf(objectValue ast.Value, defRef int) bool
- Purpose: Checks the @OneOf directive constraint on an Input Object value.
- Parameters:
- objectValue: the GraphQL value for the input object.
- defRef: reference to the input object type definition.
- Behavior:
- If the input object type does not have an @OneOf directive, returns false (no violation).
- If exactly one field is provided, validates that:
- If any field value is null, reports an error ErrOneOfInputObjectNullValue and returns true (violation).
- If any field value is a variable, and the variable's type is nullable, reports ErrOneOfInputObjectNullableVariable and returns true (violation).
- Returns: true if a violation is found, otherwise false.
- Notes: Used to enforce that a oneOf input object provides exactly one non-null field and, for variable fields, enforces nullability constraints.
Filename: v2/pkg/astvalidation/operation_rule_values.go
2) Function: objectValueSatisfiesInputValueDefinition(objectValue ast.Value, inputObjectDefinition, inputValueDefinition int) bool
- Purpose: Validates a single field value inside an Input Object value against its input value definition.
- Parameters:
- objectValue: the object value being validated.
- inputObjectDefinition: reference to the input object type definition.
- inputValueDefinition: reference to the specific field's input value definition.
- Behavior:
- Retrieves the field name and its type from the input value definition.
- Iterates over the object's provided fields to find a matching field by name and validates the field value against its definition type.
- If the field is not present and the field is not optional, reports a missing required field error ErrMissingRequiredFieldOfInputObject and returns false.
- If the field is optional or validation passes, returns true.
- Returns: true if the field satisfies its definition or is optional, false otherwise.
- Notes: Central to validating input object field values and ensuring required fields are present.
Filename: v2/pkg/astvalidation/operation_rule_values.go
3) Function: EnterArgument(ref int)
- Context: Called when traversing an Argument in the operation.
- Behavior:
- Determines the input value definition for the argument.
- If the argument value is a variable:
- Verifies the variable is defined on the operation; if not, reports ErrVariableNotDefinedOnOperation.
- Validates that the variable value satisfies the input field definition (definitionRef).
- If the argument value is a concrete value (not a variable):
- Validates that the value satisfies the input value definition type.
- Notes:
- This path handles both variables and concrete values, ensuring variables are defined on the operation before type-checking.
- Directly relevant to ensuring variables provided to inputs (including object fields) are defined, not only within oneOf contexts.
If you need, I can map these to exact code locations or extract exact excerpts.
It means validation of operations and values being sent with operations.
Summary by CodeRabbit
New Features
Tests